/**
 * Copyright (C) 2001-2018 by RapidMiner and the contributors
 * 
 * Complete list of developers available at our web site:
 * 
 * http://rapidminer.com
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.operator.meta;

import java.util.Iterator;
import java.util.List;

import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.operator.UserError;
import com.rapidminer.operator.ValueDouble;
import com.rapidminer.operator.performance.PerformanceVector;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeDouble;
import com.rapidminer.parameter.ParameterTypeInt;
import com.rapidminer.parameter.UndefinedParameterError;
import com.rapidminer.parameter.value.ParameterValueRange;
import com.rapidminer.parameter.value.ParameterValues;
import com.rapidminer.tools.RandomGenerator;
import com.rapidminer.tools.math.optimization.ec.es.ESOptimization;
import com.rapidminer.tools.math.optimization.ec.es.Individual;
import com.rapidminer.tools.math.optimization.ec.es.OptimizationValueType;


/**
 * This operator finds the optimal values for a set of parameters using an evolutionary strategies
 * approach which is often more appropriate than a grid search or a greedy search like the quadratic
 * programming approach and leads to better results. The parameter <var>parameters</var> is a list
 * of key value pairs where the keys are of the form <code>operator_name.parameter_name</code> and
 * the value for each parameter must be a semicolon separated pair of a minimum and a maximum value
 * in squared parantheses, e.g. [10;100] for a range of 10 until 100. <br/>
 * The operator returns an optimal {@link ParameterSet} which can as well be written to a file with
 * a {@link com.rapidminer.extension.legacy.operator.io.ParameterSetWriter}. This parameter set can be read in
 * another process using a {@link com.rapidminer.extension.legacy.operator.io.ParameterSetLoader}. <br/>
 * The file format of the parameter set file is straightforward and can easily be generated by
 * external applications. Each line is of the form <center>
 * <code>operator_name.parameter_name = value</code></center> <br/>
 * Please refer to section {@rapidminer.ref sec:parameter_optimization|Advanced Processes/Parameter
 * and performance analysis} for an example application.
 *
 * @author Ingo Mierswa, Tobias Malbrecht
 */
public class EvolutionaryParameterOptimizationOperator extends ParameterOptimizationOperator {

	public static final String PARAMETER_MAX_GENERATIONS = ESOptimization.PARAMETER_MAX_GENERATIONS;
	public static final String PARAMETER_GENERATIONS_WITHOUT_IMPROVAL = ESOptimization.PARAMETER_GENERATIONS_WITHOUT_IMPROVAL;
	public static final String PARAMETER_POPULATION_SIZE = ESOptimization.PARAMETER_POPULATION_SIZE;
	public static final String PARAMETER_TOURNAMENT_FRACTION = ESOptimization.PARAMETER_TOURNAMENT_FRACTION;
	public static final String PARAMETER_KEEP_BEST = ESOptimization.PARAMETER_KEEP_BEST;
	public static final String PARAMETER_MUTATION_TYPE = ESOptimization.PARAMETER_MUTATION_TYPE;
	public static final String PARAMETER_SELECTION_TYPE = ESOptimization.PARAMETER_SELECTION_TYPE;
	public static final String PARAMETER_CROSSOVER_PROB = ESOptimization.PARAMETER_CROSSOVER_PROB;
	public static final String PARAMETER_SHOW_CONVERGENCE_PLOT = ESOptimization.PARAMETER_SHOW_CONVERGENCE_PLOT;
	public static final String PARAMETER_SPECIFIY_POPULATION_SIZE = ESOptimization.PARAMETER_SPECIFIY_POPULATION_SIZE;

	// private IOContainer input;

	/** The actual optimizer. */
	private ESOptimization optimizer;
	private double bestFitnessEver = Double.NaN;
	private double lastGenerationsPerformance = Double.NaN;

	/** The operators for which parameters should be optimized. */
	private Operator[] operators;

	/** The names of the parameters which should be optimized. */
	private String[] parameters;

	/** The parameter types. */
	private OptimizationValueType[] types;

	public EvolutionaryParameterOptimizationOperator(OperatorDescription description) {
		super(description);
		addValue(new ValueDouble("best", "best performance ever") {

			@Override
			public double getDoubleValue() {
				return bestFitnessEver;
			}
		});
	}

	public Operator[] getOptimizationOperators() {
		return this.operators;
	}

	public String[] getOptimizationParameters() {
		return this.parameters;
	}

	public OptimizationValueType[] getOptimizationValueTypes() {
		return this.types;
	}

	@Override
	public int getParameterValueMode() {
		return ParameterConfigurator.VALUE_MODE_CONTINUOUS;
	}

	@Override
	public double getCurrentBestPerformance() {
		// must make this check, because optimizer will be set null to tidy up after execution
		if (optimizer != null) {
			return optimizer.getBestFitnessInGeneration();
		} else {
			return lastGenerationsPerformance;
		}
	}

	@Override
	public void doWork() throws OperatorException {

		// check parameter values list
		List<ParameterValues> parameterValuesList = parseParameterValues(getParameterList("parameters"));
		if (parameterValuesList == null) {
			throw new UserError(this, 922);
		}
		for (Iterator<ParameterValues> iterator = parameterValuesList.iterator(); iterator.hasNext();) {
			ParameterValues parameterValues = iterator.next();
			if (!(parameterValues instanceof ParameterValueRange)) {
				getLogger().warning(
						"Found (and deleted) unsupported parameter value definition. Parameters have to be given as range (e.g. as [2;5.7]).");
				iterator.remove();
			}
		}
		if (parameterValuesList.size() == 0) {
			throw new UserError(this, 922);
		}

		// get parameters to optimize
		this.operators = new Operator[parameterValuesList.size()];
		this.parameters = new String[parameterValuesList.size()];
		double[] min = new double[parameterValuesList.size()];
		double[] max = new double[parameterValuesList.size()];
		this.types = new OptimizationValueType[parameterValuesList.size()];

		int index = 0;
		for (Iterator<ParameterValues> iterator = parameterValuesList.iterator(); iterator.hasNext();) {
			ParameterValueRange parameterValueRange = (ParameterValueRange) iterator.next();
			operators[index] = parameterValueRange.getOperator();
			parameters[index] = parameterValueRange.getParameterType().getKey();
			min[index] = Double.valueOf(parameterValueRange.getMin());
			max[index] = Double.valueOf(parameterValueRange.getMax());

			ParameterType targetType = parameterValueRange.getParameterType();
			if (targetType == null) {
				throw new UserError(this, 906, parameterValueRange.getOperator() + "." + parameterValueRange.getKey());
			}
			if (targetType instanceof ParameterTypeDouble) {
				types[index] = OptimizationValueType.VALUE_TYPE_DOUBLE;
				getLogger().fine("Parameter type of parameter " + targetType.getKey() + ": double");
			} else if (targetType instanceof ParameterTypeInt) {
				types[index] = OptimizationValueType.VALUE_TYPE_INT;
				getLogger().fine("Parameter type of parameter " + targetType.getKey() + ": int");
			} else {
				throw new UserError(this, 909, targetType.getKey());
			}
			index++;
		}

		// create and start optimizer
		RandomGenerator random = RandomGenerator.getRandomGenerator(this);
		this.optimizer = createOptimizer(random);

		for (int i = 0; i < min.length; i++) {
			this.optimizer.setMin(i, min[i]);
			this.optimizer.setMax(i, max[i]);
			this.optimizer.setValueType(i, types[i]);
			this.optimizer.setExecutingOperator(this);
		}

		optimizer.optimize();

		// create result and return it
		double[] bestParameters = optimizer.getBestValuesEver();
		String[] bestValues = null;
		if (bestParameters != null) {
			bestValues = new String[bestParameters.length];
			for (int i = 0; i < bestParameters.length; i++) {
				if (types[i].equals(OptimizationValueType.VALUE_TYPE_DOUBLE)) {
					bestValues[i] = bestParameters[i] + "";
				} else {
					bestValues[i] = (int) Math.round(bestParameters[i]) + "";
				}
			}
		} else {
			bestValues = new String[operators.length];
			for (int i = 0; i < bestValues.length; i++) {
				bestValues[i] = "unknown";
			}
		}
		ParameterSet bestSet = new ParameterSet(operators, parameters, bestValues, optimizer.getBestPerformanceEver());

		// freeing memory, but saving best value before
		this.bestFitnessEver = optimizer.getBestFitnessEver();
		this.lastGenerationsPerformance = optimizer.getBestFitnessInGeneration();
		this.optimizer = null;
		deliver(bestSet);
	}

	/** This method creates a apropriate optimizer */
	protected ESOptimization createOptimizer(RandomGenerator random) throws UndefinedParameterError {
		return new ESParameterOptimization(this, operators.length, ESOptimization.INIT_TYPE_RANDOM,
				getParameterAsInt(PARAMETER_MAX_GENERATIONS), getParameterAsInt(PARAMETER_GENERATIONS_WITHOUT_IMPROVAL),
				getParameterAsInt(PARAMETER_POPULATION_SIZE), getParameterAsInt(PARAMETER_SELECTION_TYPE),
				getParameterAsDouble(PARAMETER_TOURNAMENT_FRACTION), getParameterAsBoolean(PARAMETER_KEEP_BEST),
				getParameterAsInt(PARAMETER_MUTATION_TYPE), getParameterAsDouble(PARAMETER_CROSSOVER_PROB),
				getParameterAsBoolean(PARAMETER_SHOW_CONVERGENCE_PLOT), random, this);
	}

	public ESOptimization getOptimization() {
		return optimizer;
	}

	public PerformanceVector setParametersAndEvaluate(Individual individual) throws OperatorException {
		double[] currentValues = individual.getValues();
		for (int j = 0; j < currentValues.length; j++) {
			String value;
			if (types[j].equals(OptimizationValueType.VALUE_TYPE_DOUBLE)) {
				value = currentValues[j] + "";
			} else {
				value = (int) Math.round(currentValues[j]) + "";
			}
			operators[j].getParameters().setParameter(parameters[j], value);
			getLogger().fine(operators[j] + "." + parameters[j] + " = " + value);
		}
		return getPerformanceVector();
	}

	@Override
	public List<ParameterType> getParameterTypes() {
		List<ParameterType> types = super.getParameterTypes();
		types.addAll(ESOptimization.getParameterTypes(this));
		return types;
	}

	public int getNumberOfOptimizationParameters() {
		return this.parameters.length;
	}
}
